from tkinter import *
from tkinter.ttk import *
import random
import sys
from pulp import *

length=5
pixel_size=50

min_level=1
max_level=5

recursionlimit_level=(max_level-min_level)//2+min_level
sys.setrecursionlimit((recursionlimit_level+5)*10)

min_line_count=2+(recursionlimit_level*2)
size_w=20
size_h=20

first_pos=(0,0)
first_line=True
current_line_count=0
roll_flag=False

create_lock=False
text_groups=None

def get_new_result():
    global current_line_count,roll_flag,first_pos,first_line
    board=[]
    first_pos=(0,0)
    roll_flag=False
    
    def get_board(pos_x,pos_y,default=2):
        try:
            return board[pos_y][pos_x]
        except IndexError:
            return default

    def safe_remove(l,v):
        if v in l:
            l.remove(v)

    def full_board(board_obj):
        for i in range(size_w):
            board_obj.append([])
            for j in range(size_h):
                board_obj[i].append(0)

    def gen_line(pos:tuple,unable_dir=None):
        global first_pos,roll_flag,current_line_count,first_line
        full_dir_list=[(-1,0),(1,0),(0,-1),(0,1)]
        half_dir_list=[(-1,0),(1,0),(0,-1),(0,1)]
        current_line_count+=1
        #roll_back
        roll_dir=None
        half_flag=False
        if current_line_count>=min_line_count:
            if (pos[0]-length,pos[1])==first_pos:
                roll_dir=(-1,0)
            elif (pos[0]+length,pos[1])==first_pos:
                roll_dir=(1,0)
            elif (pos[0],pos[1]-length)==first_pos:
                roll_dir=(0,-1)
            elif (pos[0],pos[1]+length)==first_pos:
                roll_dir=(0,1)
            elif (pos[0]-length//2,pos[1])==first_pos and unable_dir!=0 and not get_board(pos[0]+(length//2),pos[1]):
                roll_dir=(-1,0)
                half_flag=True
            elif (pos[0]+length//2,pos[1])==first_pos and unable_dir!=0 and not get_board(pos[0]-(length//2),pos[1]):
                roll_dir=(1,0)
                half_flag=True
            elif (pos[0],pos[1]-length//2)==first_pos and unable_dir!=1 and not get_board(pos[0],pos[1]+(length//2)):
                roll_dir=(0,-1)
                half_flag=True
            elif (pos[0],pos[1]+length//2)==first_pos and unable_dir!=1 and not get_board(pos[0],pos[1]-(length//2)):
                roll_dir=(0,1)
                half_flag=True
        if roll_dir!=None:
            if half_flag:
                if roll_dir==(-1,0) or roll_dir==(1,0):
                    for i in (1,2):
                        board[pos[1]][pos[0]-i]=1
                        board[pos[1]][pos[0]+i]=1
                elif roll_dir==(0,-1) or roll_dir==(0,1):
                    for i in (1,2):
                        board[pos[1]-i][pos[0]]=1
                        board[pos[1]+i][pos[0]]=1
            else:
                if roll_dir==(-1,0):
                    for i in range(length):
                        board[pos[1]][pos[0]-i]=1
                elif roll_dir==(1,0):
                    for i in range(length):
                        board[pos[1]][pos[0]+i]=1
                elif roll_dir==(0,-1):
                    for i in range(length):
                        board[pos[1]-i][pos[0]]=1
                elif roll_dir==(0,1):
                    for i in range(length):
                        board[pos[1]+i][pos[0]]=1
            roll_flag=True
            return
        
        if unable_dir==0:
            full_dir_list.remove((-1,0))
            full_dir_list.remove((1,0))
            half_dir_list.remove((-1,0))
            half_dir_list.remove((1,0))
        elif unable_dir==1:
            full_dir_list.remove((0,-1))
            full_dir_list.remove((0,1))
            half_dir_list.remove((0,-1))
            half_dir_list.remove((0,1))
            
        if pos[0]-length<0 or get_board(pos[0]-length-1,pos[1]) or get_board(pos[0]-(length//2),pos[1]):
            safe_remove(full_dir_list,(-1,0))
        if pos[0]-length>=size_w or get_board(pos[0]+length-1,pos[1]) or get_board(pos[0]+(length//2),pos[1]):
            safe_remove(full_dir_list,(1,0))
        if pos[1]-length<0 or get_board(pos[0],pos[1]-length-1) or get_board(pos[0],pos[1]-(length//2)):
            safe_remove(full_dir_list,(0,-1))
        if pos[1]-length>=size_h or get_board(pos[0],pos[1]+length-1) or get_board(pos[0],pos[1]+(length//2)):
            safe_remove(full_dir_list,(0,1))
        
        if pos[0]-(length//2)<0 or get_board(pos[0]-(length//2),pos[1]) or pos[0]+(length//2)>=size_w or get_board(pos[0]+(length//2),pos[1]):
            safe_remove(half_dir_list,(-1,0))
            safe_remove(half_dir_list,(1,0))
        if pos[1]-(length//2)<0 or get_board(pos[0],pos[1]-(length//2)) or pos[1]+(length//2)>=size_h or get_board(pos[0],pos[1]+(length//2)):
            safe_remove(half_dir_list,(0,-1))
            safe_remove(half_dir_list,(0,1))
        
        if len(full_dir_list)!=0 and len(half_dir_list)!=0:    
            is_full=random.randint(0,1) if not first_line else 1
            if is_full:
                dir=random.choice(full_dir_list)
            else:
                dir=random.choice(half_dir_list)
        elif len(full_dir_list)!=0 and len(half_dir_list)==0:
            is_full=1
            dir=random.choice(full_dir_list)
        elif len(full_dir_list)==0 and len(half_dir_list)!=0:
            is_full=0
            dir=random.choice(half_dir_list)
        else:
            return
        
        last_point=None
        
        if is_full:
            if dir==(-1,0):
                for i in range(length):
                    board[pos[1]][pos[0]-i]=1
                last_point=(pos[0]-i,pos[1])
            elif dir==(1,0):
                for i in range(length):
                    board[pos[1]][pos[0]+i]=1
                last_point=(pos[0]+i,pos[1])
            elif dir==(0,-1):
                for i in range(length):
                    board[pos[1]-i][pos[0]]=1
                last_point=(pos[0],pos[1]-i)
            elif dir==(0,1):
                for i in range(length):
                    board[pos[1]+i][pos[0]]=1
                last_point=(pos[0],pos[1]+i)
        else:
            if dir==(-1,0) or dir==(1,0):
                board[pos[1]][pos[0]]=1
                for i in (1,2):
                    board[pos[1]][pos[0]-i]=1
                    board[pos[1]][pos[0]+i]=1
                last_point=(pos[0]+i,pos[1])
            elif dir==(0,-1) or dir==(0,1):
                board[pos[1]][pos[0]]=1
                for i in (1,2):
                    board[pos[1]-i][pos[0]]=1
                    board[pos[1]+i][pos[0]]=1
                last_point=(pos[0],pos[1]+i)
        
        new_unable_dir=None
        if dir==(-1,0) or dir==(1,0):
            new_unable_dir=0
        elif dir==(0,-1) or dir==(0,1):
            new_unable_dir=1
        try:
            first_line=False
            if gen_line(last_point,new_unable_dir)==None:
                return
        except RecursionError:
            return

    while True:
        board=[]
        full_board(board)
        current_line_count=0
        roll_flag=False

        first_line=True
        first_pos=(random.randint(0,size_w/length-1)*length,random.randint(0,size_h/length-1)*length)
        gen_line(first_pos)
        if roll_flag:
            break
    return board

def find_line(groups,pos):
    lines=[]
    for obj in groups:
        if obj[1]==pos[1]:
            lines.append(obj)
    return lines

def find_row(groups,pos):
    rows=[]
    for obj in groups:
        if obj[0]==pos[0]:
            rows.append(obj)
    return rows

def check_same_line(same_line_groups,pos,space=2):
    for line in same_line_groups:
        if line[0]+space==pos[0] and line[1]==pos[1]:
            return line
    return False

def check_same_row(same_row_groups,pos,space=2):
    for row in same_row_groups:
        if row[0]==pos[0] and row[1]+space==pos[1]:
            return row
    return False

def scan_lines(board:list):
    def get_board(pos_x,pos_y,default=0):
        try:
            return board[pos_y][pos_x]
        except IndexError:
            return default
    
    start_index_x=-1
    start_index_y=-1
    for y in range(0,size_h):
        for x in range(0,size_w):
            if get_board(x,y)==1:
                start_index_y=y % 2
                break
        if start_index_y!=-1:
            break
        
    for x in range(0,size_w):
        for y in range(0,size_h):
            if get_board(x,y)==1:
                start_index_x=x % 2
                break
        if start_index_x!=-1:
            break  

    calc_groups=[]
    line_groups={}
    row_groups={}
    for y in range(start_index_y,size_h,2):
        for x in range(start_index_x,size_w,2):
            if board[y][x]==0:
                continue
            pos=(x,y)
            l=check_same_line(find_line(line_groups,pos),pos)
            if l!=False and get_board(x-1,y)==1:
                pass
            else:
                if get_board(x,y) + get_board(x+1,y) + get_board(x+2,y) + get_board(x+3,y) + get_board(x+4,y)==5:
                    line_groups[pos]=((x,y),(x+1,y),(x+2,y),(x+3,y),(x+4,y))
                    calc_groups.append(line_groups[pos])
            r=check_same_row(find_row(row_groups,pos),pos)
            if r!=False and get_board(x,y-1)==1:
                pass
            else:
                if get_board(x,y) + get_board(x,y+1) + get_board(x,y+2) + get_board(x,y+3) + get_board(x,y+4)==5:
                    row_groups[pos]=((x,y),(x,y+1),(x,y+2),(x,y+3),(x,y+4))
                    calc_groups.append(row_groups[pos])
    return calc_groups

def search_head(groups,pos,self_index):
    result=[]
    n=0
    for group in groups:
        if self_index==n:
            n+=1
            continue
        if pos == group[0]:          
            result.append(n)
        n+=1
    return result

def search_mid(groups,pos,self_index):
    result=[]
    n=0
    for group in groups:
        if self_index==n:
            n+=1
            continue
        if pos == group[2]:
            result.append(n)
        n+=1
    return result

def search_tail(groups,pos,self_index):
    result=[]
    n=0
    for group in groups:
        if self_index==n:
            n+=1
            continue
        if pos == group[4]:
            result.append(n)
        n+=1
    return result

def get_new_calc(board:list,max_result=10,allow_minus=False):
    calc_groups=scan_lines(board)
    loop_count=0
    while True:
        loop_count+=1
        if loop_count>100:
            return None
        # problem = LpProblem("Loop problem", LpMaximize)
        problem = LpProblem("Loop problem", LpMinimize)
        g_one=[]
        g_two=[]
        g_abs_two=[]
        g_result=[]

        for n in range(len(calc_groups)):
            g_one.append(LpVariable(f"g_one_{n}",lowBound=0,upBound=max_result,cat=LpInteger))
            if allow_minus:
                g_two.append(LpVariable(f"g_two_{n}",lowBound=-max_result,upBound=max_result,cat=LpInteger))
            else:
                g_two.append(LpVariable(f"g_two_{n}",lowBound=0,upBound=max_result,cat=LpInteger))
            g_abs_two.append(LpVariable(f"g_abs_two_{n}",lowBound=0,upBound=max_result,cat=LpInteger))
            g_result.append(LpVariable(f"g_result_{n}",lowBound=0,upBound=max_result,cat=LpInteger))
            
        for i in range(len(calc_groups)):
            problem += g_result[i]==g_one[i]+g_two[i]
            problem += g_one[i]>=random.choice([0,1,1,1,1])

        random_g_one=set()

        n=0
        for group in calc_groups:
            for p in [0,2,4]:
                r1=search_head(calc_groups,group[p],n)
                if len(r1)>0:
                    if p==0:
                        for r in r1:
                            problem += g_one[n]==g_one[r]
                            random_g_one.add(n)
                            random_g_one.add(r)
                    if p==2:
                        for r in r1:
                            problem += g_two[n]==g_one[r]
                            problem += g_abs_two[n]>=g_two[r]
                            problem += g_abs_two[n]<=g_two[r]
                    if p==4:
                        for r in r1:
                            problem += g_result[n]==g_one[r]
                r2=search_mid(calc_groups,group[p],n)
                if len(r2)>0:
                    if p==0:
                        for r in r2:
                            problem += g_one[n]==g_two[r]
                            problem += g_abs_two[n]>=g_two[r]
                            problem += g_abs_two[n]<=g_two[r]
                            random_g_one.add(n)
                            random_g_one.add(r)
                    if p==2:
                        for r in r2:
                            problem += g_two[n]==g_two[r]
                            problem += g_abs_two[n]>=g_two[r]
                            problem += g_abs_two[n]<=g_two[r]
                    if p==4:
                        for r in r2:
                            problem += g_result[n]==g_two[r]
                            problem += g_abs_two[n]>=g_two[r]
                            problem += g_abs_two[n]<=g_two[r]
                r3=search_tail(calc_groups,group[p],n)
                if len(r3)>0:
                    if p==0:
                        for r in r3:
                            problem += g_one[n]==g_result[r]
                            random_g_one.add(n)
                            random_g_one.add(r)
                    if p==2:
                        for r in r3:
                            problem += g_two[n]==g_result[r]
                            problem += g_abs_two[n]>=g_two[r]
                            problem += g_abs_two[n]<=g_two[r]
                    if p==4:
                        for r in r3:
                            problem += g_result[n]==g_result[r]
            n+=1 

        random_count=0
        for index in random_g_one:
            if random.random()>0.5:
                problem += g_one[index]==random.randint(1,max_result)
                random_count+=1
            
        status=problem.solve()
        if status==1:
            break
    
    result_groups={}
    for n in range(len(calc_groups)):
        result_groups[calc_groups[n][0]]=str(int(g_one[n].varValue))
        result_groups[calc_groups[n][1]]="+" if g_two[n].varValue>=0 else "-"
        result_groups[calc_groups[n][2]]=str(int(abs(g_two[n].varValue)))
        result_groups[calc_groups[n][3]]="="
        result_groups[calc_groups[n][4]]=str(int(g_result[n].varValue))
    
    result=[]
    for i in range(len(calc_groups)):
        result.append(str(g_one[i].varValue) + "+" + str(g_two[i].varValue) + "=" + str(g_result[i].varValue))
    return result_groups
    


root=Tk()
var_minus_switch=BooleanVar()
var_minus_switch.set(False)
var_max_result=IntVar()
var_max_result.set(10)
var_board_size=IntVar()
var_board_size.set(20)

def config_window(event):
    def set_size(value):
        global size_w,size_h
        if value==1:
            size_w=15
            size_h=15
        if value==2:
            size_w=20
            size_h=20
        root.geometry(f"{size_w*pixel_size}x{size_h*pixel_size}")
    
    window=Toplevel(root)
    window.title("配置")
    
    frame_minus=Frame(window)
    frame_minus.pack(side=TOP)
    check_minus=Checkbutton(frame_minus,text="增加减法运算",variable=var_minus_switch)
    check_minus.pack(side=LEFT)
    
    frame_max_result=Frame(window)
    frame_max_result.pack(side=TOP)
    label_max_result=Label(frame_max_result,text="最大结果:(10-100)")
    label_max_result.pack(side=LEFT)
    entry_max_result=Entry(frame_max_result,textvariable=var_max_result)
    entry_max_result.pack(side=LEFT)
    
    frame_size=Frame(window)
    frame_size.pack(side=TOP)
    Button_min_size=Radiobutton(frame_size,text="最小尺寸",variable=var_board_size,value="15",command=lambda:set_size(1))
    Button_min_size.pack(side=LEFT)
    Button_max_size=Radiobutton(frame_size,text="最大尺寸",variable=var_board_size,value="20",command=lambda:set_size(2))
    Button_max_size.pack(side=RIGHT)
    
    button_config_ok=Button(window,text="确定",command=lambda:config_window_ok(window))
    button_config_ok.pack(side=LEFT)
    
    button_config_cancel=Button(window,text="取消",command=lambda:config_window_cancel(window))
    button_config_cancel.pack(side=RIGHT)
    
    button_config_ok.focus_set()
    
def config_window_ok(window):
    global max_result
    max_result=var_max_result.get()
    if max_result<10:
        max_result=10
    if max_result>100:
        max_result=100
    window.destroy()
    
def config_window_cancel(window):
    window.destroy()

title_instructions="算术棋盘 F2:配置 回车键重新生成 滚轮修改生成难度 鼠标右键显示答案  当前最高难度等级:"
title_str=f"{title_instructions}{recursionlimit_level}"
root.title(title_str)
root.resizable(False,False)
root.geometry(f"{size_w*pixel_size}x{size_h*pixel_size}")
board_canvas=Canvas(root,width=size_w*pixel_size,height=size_h*pixel_size)
# board_canvas.config(bg="black")
board_canvas.config(bg="white")
board_canvas.pack()


def change_level(event):
    global recursionlimit_level,title_str,min_line_count
    if event.delta>0:
        recursionlimit_level+=1
        if recursionlimit_level>max_level:
            recursionlimit_level=max_level
        sys.setrecursionlimit((recursionlimit_level+5)*10)
    elif event.delta<0:
        recursionlimit_level-=1
        if recursionlimit_level<min_level:
            recursionlimit_level=min_level
        sys.setrecursionlimit((recursionlimit_level+5)*10)
    min_line_count=2+(recursionlimit_level*2)
    title_str=f"{title_instructions}{recursionlimit_level}"
    root.title(title_str)

def update_board_canvas(canvas_obj:Canvas):
    global create_lock,text_groups
    if create_lock:
        return
    create_lock=True
    canvas_obj.delete("all")
    canvas_obj.create_text(size_w*pixel_size//2,size_h*pixel_size//2,text="生成中...",font=("黑体",pixel_size),fill="green")
    canvas_obj.update()
    while True:
        board_obj=get_new_result()
        text_groups=get_new_calc(board_obj,allow_minus=var_minus_switch.get(),max_result=var_max_result.get())
        if text_groups!=None:
            break
    canvas_obj.delete("all")
    for i in range(size_h):
        for j in range(size_w):
            if board_obj[j][i]==1:
                canvas_obj.create_rectangle(i*pixel_size,j*pixel_size,(i+1)*pixel_size,(j+1)*pixel_size,fill="white")
    for pos in text_groups:
        if text_groups[pos] in ["+","-","="]:
            canvas_obj.create_text(pos[0]*pixel_size+pixel_size//2,pos[1]*pixel_size+pixel_size//2,text=text_groups[pos],font=("Arial",pixel_size//3),fill="black",tag='text')
    
    
    calc_groups=scan_lines(board_obj)
    already_show=[]
    count_groups={}
    detail_every_group={}
    mutli_point_pos=set()
    for group in calc_groups:
        count_groups[group]=0
        p_index=0
        for pos in group:
            if p_index in [1,3]:
                p_index+=1
                continue
            if pos in detail_every_group:
                detail_every_group[pos].append(group)
                mutli_point_pos.add(pos)
            else:
                detail_every_group[pos]=[group]
            p_index+=1
    
    for pos_group in calc_groups:
        if count_groups[pos_group]==0:
            _flag=0
            for _index in [0,2,4]:
                if pos_group[_index] not in mutli_point_pos:
                    _flag+=1
                    canvas_obj.create_text(pos_group[_index][0]*pixel_size+pixel_size//2,pos_group[_index][1]*pixel_size+pixel_size//2,text=text_groups[pos_group[_index]],font=("Arial",pixel_size//3),fill="black",tag='text')
                    already_show.append(pos_group[_index])
                    count_groups[pos_group]+=1
                    break
            if not _flag:
                _index = random.choice([0,2,4])
                canvas_obj.create_text(pos_group[_index][0]*pixel_size+pixel_size//2,pos_group[_index][1]*pixel_size+pixel_size//2,text=text_groups[pos_group[_index]],font=("Arial",pixel_size//3),fill="black",tag='text')
                already_show.append(pos_group[_index])
                for p_group in detail_every_group[pos_group[_index]]:
                    count_groups[p_group]+=1
    max_loop=1000
    while True:
        start_group=random.choice(calc_groups)
        if count_groups[start_group]<2:
            break
        max_loop-=1
        if max_loop==0:
            create_lock=False
            return
    for pos in start_group:
        if pos in already_show:
            continue
        canvas_obj.create_text(pos[0]*pixel_size+pixel_size//2,pos[1]*pixel_size+pixel_size//2,text=text_groups[pos],font=("Arial",pixel_size//3),fill="black",tag='text')
        break            
    create_lock=False
    
def show_answer(canvas_obj:Canvas):
    if text_groups==None:
        return
    canvas_obj.delete("text")
    for pos in text_groups:
        canvas_obj.create_text(pos[0]*pixel_size+pixel_size//2,pos[1]*pixel_size+pixel_size//2,text=text_groups[pos],font=("Arial",pixel_size//3),fill="black",tag='text')
    board_canvas.update()
    
update_board_canvas(board_canvas)

board_canvas.bind_all("<F2>",config_window)
board_canvas.bind_all("<Return>",lambda event:update_board_canvas(board_canvas))
board_canvas.bind_all("<Button-3>",lambda event:show_answer(board_canvas))
board_canvas.bind_all("<MouseWheel>",change_level)
root.mainloop()